package com.winhxd.b2c.search.service.impl;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.winhxd.b2c.common.constant.BusinessCode;
import com.winhxd.b2c.common.domain.product.model.ProductPriceSpu;
import com.winhxd.b2c.common.domain.product.model.ProductSku;
import com.winhxd.b2c.common.domain.product.model.ProductSpuImage;
import com.winhxd.b2c.common.domain.product.vo.ProductVO;
import com.winhxd.b2c.common.domain.search.dto.ProductSearchParam;
import com.winhxd.b2c.common.domain.search.dto.SearchResult;
import com.winhxd.b2c.common.domain.search.dto.SkuChanged;
import com.winhxd.b2c.common.domain.search.model.ProductDocument;
import com.winhxd.b2c.common.exception.BusinessException;
import com.winhxd.b2c.search.util.JsonUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.entity.ContentType;
import org.apache.http.nio.entity.NStringEntity;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.avg.Avg;
import org.elasticsearch.search.aggregations.metrics.sum.Sum;
import org.elasticsearch.search.aggregations.metrics.tophits.TopHits;
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregatorBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class ProductSearchServiceImpl implements com.winhxd.b2c.search.service.ProductSearchService {
    private static final Logger logger = LoggerFactory.getLogger(ProductSearchServiceImpl.class);

    private static final String PRODUCT_INDEX_NAME = "outlet-product";
    private static final String PRODUCT_TYPE_NAME = "_doc";
    private static final String PRODUCT_GROUP_FIELD = "spuColor";

    @Resource
    private RestHighLevelClient esClient;

    @Override
    public SearchResult<ProductDocument> search(ProductSearchParam param) {
        AggregationBuilder aggregation = buildAggregationBuilder(param);
        QueryBuilder queryBuilder = buildQueryBuilder(param);
        SearchSourceBuilder builder = SearchSourceBuilder.searchSource()
                .size(0)
                .aggregation(aggregation)
                .query(queryBuilder);
        SearchRequest req = new SearchRequest(PRODUCT_INDEX_NAME).source(builder);
        List<ProductDocument> list = new ArrayList<>();
        try {
            SearchResponse result = esClient.search(req);
            if (result.status().getStatus() == HttpStatus.OK.value()) {
                Terms t = result.getAggregations().get(PRODUCT_GROUP_FIELD);
                for (Terms.Bucket bucket : t.getBuckets()) {
                    TopHits hits = bucket.getAggregations().get("skuList");
                    Sum salesVolume = bucket.getAggregations().get("salesVolume");
                    Avg settlementPrice = bucket.getAggregations().get("settlementPrice");
                    String source = hits.getHits().getAt(0).getSourceAsString();
                    ProductDocument product = JsonUtil.toObject(source, ProductDocument.class);
                    product.setSalesVolume((long) salesVolume.getValue());
                    product.setSettlementPrice(new BigDecimal(settlementPrice.getValue()).setScale(2, RoundingMode.HALF_UP));
                    list.add(product);
                }
            }
        } catch (Exception ex) {
            logger.error("=商品搜索=查询异常:" + JsonUtil.toJson(param), ex);
            throw new BusinessException(BusinessCode.CODE_1001);
        }
        return new SearchResult<>(list);
    }


    @Override
    public void importProduct(List<ProductVO> productVOList) {
        try {
            List<Long> spuIds = productVOList.stream().map(ProductVO::getSpuId).collect(Collectors.toList());
            logger.info("=商品搜索=导入数据开始:" + spuIds);

            deleteSpuList(spuIds);

            BulkRequest br = new BulkRequest();
            for (ProductVO spu : productVOList) {
                ProductDocument prod = new ProductDocument();
                prod.setSpuName(spu.getSpuName());
                prod.setSpuId(spu.getSpuId());
                prod.setBrandId(spu.getBrandId());
                prod.setBrandName(spu.getBrandName());
                prod.setGuidePrice(spu.getGuidePrice());
                if (CollectionUtils.isNotEmpty(spu.getProductTagList())) {
                    prod.setTags(spu.getProductTagList().stream().map(li -> {
                        ProductDocument.Tag tag = new ProductDocument.Tag();
                        BeanUtils.copyProperties(li, tag);
                        return tag;
                    }).collect(Collectors.toList()));
                }
                if (CollectionUtils.isNotEmpty(spu.getProductCategoryGroupList())) {
                    prod.setCategories(spu.getProductCategoryGroupList().stream().map(li -> {
                        ProductDocument.Category category = new ProductDocument.Category();
                        BeanUtils.copyProperties(li, category);
                        return category;
                    }).collect(Collectors.toList()));
                }

                for (ProductSku sku : spu.getProductSkuList()) {
                    ProductDocument prodSku = new ProductDocument();
                    BeanUtils.copyProperties(prod, prodSku);
                    if (StringUtils.isNotBlank(sku.getImageUrl())) {
                        prodSku.setImages(Lists.newArrayList(sku.getImageUrl()));
                    }
                    prodSku.setSkuId(sku.getId());
                    prodSku.setSkuTime(sku.getCreatedTime());
                    prodSku.setSpuColor(spu.getSpuId() + "_" + sku.getColorAttributeOptionId());
                    prodSku.setSkuStyleCode(sku.getSkuStyleCode());
                    prodSku.setStock(sku.getStock());
                    prodSku.setSalesVolume(sku.getSalesVolume());
                    prodSku.setColorAttributeOptionId(sku.getColorAttributeOptionId());
                    prodSku.setColorAttributeOption(spu.getProductAttributeList().stream()
                            .filter(li -> li.getAttributeId().equals(sku.getColorAttributeId()))
                            .findFirst().get().getAttributeOptionList().stream()
                            .filter(li -> li.getId().equals(sku.getColorAttributeOptionId())).findFirst().get().getOptionValue());
                    prodSku.setSizeAttributeOptionId(sku.getSizeAttributeOptionId());
                    prodSku.setSizeAttributeOption(spu.getProductAttributeList().stream()
                            .filter(li -> li.getAttributeId().equals(sku.getSizeAttributeId()))
                            .findFirst().get().getAttributeOptionList().stream()
                            .filter(li -> li.getId().equals(sku.getSizeAttributeOptionId())).findFirst().get().getOptionValue());

                    for (ProductPriceSpu priceSpu : spu.getProductPriceSpuList()) {
                        ProductDocument prodSkuPrice = new ProductDocument();
                        BeanUtils.copyProperties(prodSku, prodSkuPrice);
                        prodSkuPrice.setSettlementPrice(priceSpu.getSettlementPrice());
                        prodSkuPrice.setPriceProgramId(priceSpu.getPriceProgramId());

                        br.add(
                                new IndexRequest(PRODUCT_INDEX_NAME, PRODUCT_TYPE_NAME, prodSkuPrice.getSkuId() + "_" + prodSkuPrice.getPriceProgramId())
                                        .routing(prodSkuPrice.getSpuColor())
                                        .source(JsonUtil.toJson(prodSkuPrice), XContentType.JSON)
                        );
                    }
                }
            }

            BulkResponse bulk = esClient.bulk(br);
            if (bulk.hasFailures()) {
                logger.warn("=商品搜索=导入数据错误:" + bulk.buildFailureMessage());
            } else {
                logger.info("=商品搜索=导入数据成功");
            }
        } catch (Exception e) {
            logger.error("=商品搜索=导入数据异常", e);
            throw new BusinessException(BusinessCode.CODE_1001);
        }
    }

    @Override
    public void deleteSpuList(List<Long> spuIds) {
        logger.info("=商品搜索=删除数据开始:" + spuIds);
        try {
            Map query = ImmutableMap.of("query", ImmutableMap.of("terms", ImmutableMap.of("spuId", spuIds)));
            NStringEntity entity = new NStringEntity(JsonUtil.toJson(query), ContentType.APPLICATION_JSON);
            Response response = esClient.getLowLevelClient().performRequest(
                    "POST",
                    "/" + PRODUCT_INDEX_NAME + "/_delete_by_query",
                    Collections.emptyMap(),
                    entity
            );
            if (response.getStatusLine().getStatusCode() != HttpStatus.OK.value()) {
                logger.warn("=商品搜索=删除数据错误:" + response.toString());
            } else {
                String r = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
                logger.info("=商品搜索=删除数据成功:" + r);
            }
        } catch (Exception ex) {
            logger.error("=商品搜索=删除数据异常", ex);
        }
    }

    @Override
    public void updateSku(List<SkuChanged> changes) {
        try {
            List<Long> skuIds = new ArrayList<>(changes.size());
            BulkRequest br = new BulkRequest();
            for (SkuChanged sku : changes) {
                skuIds.add(sku.getSkuId());
                for (Long priceProgramId : sku.getPriceProgramIdList()) {
                    Map<String, Long> map = new HashMap<>(2);
                    if (sku.getStock() != null) {
                        map.put("stock", sku.getStock());
                    }
                    if (sku.getSalesVolume() != null) {
                        map.put("salesVolume", sku.getSalesVolume());
                    }

                    br.add(new UpdateRequest(PRODUCT_INDEX_NAME, PRODUCT_TYPE_NAME, sku.getSkuId() + "_" + priceProgramId)
                            .doc(map).routing(sku.getSpuId() + "_" + sku.getColorAttributeOptionId()));
                }
            }

            logger.info("=商品搜索=更新数据开始:" + skuIds);
            BulkResponse bulk = esClient.bulk(br);
            if (bulk.hasFailures()) {
                logger.warn("=商品搜索=更新数据错误:" + bulk.buildFailureMessage());
            } else {
                logger.info("=商品搜索=更新数据成功");
            }
        } catch (Exception e) {
            logger.error("=商品搜索=更新数据异常", e);
            throw new BusinessException(BusinessCode.CODE_1001);
        }
    }

    private static QueryBuilder buildQueryBuilder(ProductSearchParam param) {
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        queryBuilder.must(QueryBuilders.termQuery("priceProgramId", param.getPriceProgramId()));
        if (CollectionUtils.isNotEmpty(param.getCategoryOneIdList())) {
            queryBuilder.must(QueryBuilders.termsQuery("categories.categoryOneId", param.getCategoryOneIdList()));
        }
        if (CollectionUtils.isNotEmpty(param.getCategoryTwoIdList())) {
            queryBuilder.must(QueryBuilders.termsQuery("categories.categoryTwoId", param.getCategoryTwoIdList()));
        }
        if (CollectionUtils.isNotEmpty(param.getColorAttributeOptionIdList())) {
            queryBuilder.must(QueryBuilders.termsQuery("colorAttributeOptionId", param.getColorAttributeOptionIdList()));
        }
        if (CollectionUtils.isNotEmpty(param.getSizeAttributeOptionIdList())) {
            queryBuilder.must(QueryBuilders.termsQuery("sizeAttributeOptionId", param.getSizeAttributeOptionIdList()));
        }
        if (CollectionUtils.isNotEmpty(param.getTagIdList())) {
            queryBuilder.must(QueryBuilders.termsQuery("tags.id", param.getTagIdList()));
        }
        if (StringUtils.isNotBlank(param.getSearchText())) {
            MultiMatchQueryBuilder textQuery = QueryBuilders.multiMatchQuery(
                    param.getSearchText(),
                    "spuName", "categories.categoryOneName", "categories.categoryTwoName",
                    "tags.name", "colorAttributeOption", "skuStyleCode")
                    .type(MultiMatchQueryBuilder.Type.CROSS_FIELDS).analyzer("ik_smart")
                    .operator(Operator.AND);
            queryBuilder.must(textQuery);
        }
        return queryBuilder;
    }

    private static AggregationBuilder buildAggregationBuilder(ProductSearchParam param) {
        int pageNo = param.getPageNo();
        int pageSize = param.getPageSize();
        TermsAggregationBuilder aggregation = AggregationBuilders.terms(PRODUCT_GROUP_FIELD).field(PRODUCT_GROUP_FIELD).size(Integer.MAX_VALUE)
                .subAggregation(AggregationBuilders.topHits("skuList").size(1))
                .subAggregation(AggregationBuilders.sum("salesVolume").field("salesVolume"))
                .subAggregation(AggregationBuilders.avg("settlementPrice").field("settlementPrice"))
                .subAggregation(AggregationBuilders.max("skuTime").field("skuTime"));
        List<FieldSortBuilder> sorts = null;
        switch (Optional.ofNullable(param.getSortType()).orElse(ProductSearchParam.SortType.NONE)) {
            case PRICE_ASC:
                sorts = Lists.newArrayList(new FieldSortBuilder("settlementPrice").order(SortOrder.ASC));
                break;
            case PRICE_DESC:
                sorts = Lists.newArrayList(new FieldSortBuilder("settlementPrice").order(SortOrder.DESC));
                break;
            case SALES_DESC:
                sorts = Lists.newArrayList(new FieldSortBuilder("salesVolume").order(SortOrder.DESC));
                break;
            case TIME_DESC:
                sorts = Lists.newArrayList(new FieldSortBuilder("skuTime").order(SortOrder.DESC));
                break;
            default:
                break;
        }
        aggregation.subAggregation(PipelineAggregatorBuilders.bucketSort("product_sort", sorts)
                .from((pageNo - 1) * pageSize).size(pageSize));
        return aggregation;
    }
}
